from django.db.models import Q, Manager
from seeker.utils import parse_sort_field
from seeker.models import SavedSearch
import seeker

TEXT_OPERATORS = (
	('exact', 'Equals'),
	('icontains', 'Contains'),
	('istartswith', 'Starts With'),
	('iendswith', 'Ends With'),
	('iregex', 'Matches'),
)

NUMERIC_OPERATORS = (
	('exact', 'Equals'),
	('gt', 'Greater Than'),
	('lt', 'Less Than'),
)

LOOKUP_OPERATORS = (
	('exact', 'Is'),
	('isnot', 'Is Not'),
)

DATE_OPERATORS = (
	('exact', 'Is'),
	('lt', 'Is Before'),
	('gt', 'Is After'),
)

FIELD_INFO = {
	'CharField': (seeker.TYPE_TEXT, TEXT_OPERATORS),
	'TextField': (seeker.TYPE_TEXT, TEXT_OPERATORS),
	'SlugField': (seeker.TYPE_TEXT, TEXT_OPERATORS),
	'AutoField': (seeker.TYPE_NUMERIC, NUMERIC_OPERATORS),
	'IntegerField': (seeker.TYPE_NUMERIC, NUMERIC_OPERATORS),
	'SmallIntegerField': (seeker.TYPE_NUMERIC, NUMERIC_OPERATORS),
	'PositiveIntegerField': (seeker.TYPE_NUMERIC, NUMERIC_OPERATORS),
	'PositiveSmallIntegerField': (seeker.TYPE_NUMERIC, NUMERIC_OPERATORS),
	'DecimalField': (seeker.TYPE_NUMERIC, NUMERIC_OPERATORS),
	'FloatField': (seeker.TYPE_NUMERIC, NUMERIC_OPERATORS),
	'BooleanField': (seeker.TYPE_BOOLEAN, (('exact','Is'),)),
	'DateField': (seeker.TYPE_DATE, DATE_OPERATORS),
	'DateTimeField': (seeker.TYPE_DATE, DATE_OPERATORS),
}

def capitalize( value ):
	return " ".join( [s[0].upper() + s[1:] for s in value.split()] )

def _aggregate( manager, value, field ):
	name = field.split('__')[-1]
	func = getattr( value, 'aggregate_%s' % name, None )
	if func is not None:
		return func()
	return manager.count()

def _follow( value, field, default=None ):
	parent = None
	for part in field.split('__'):
		parent = value
		value = getattr( value, part, None )
		if value is None:
			return default
	if isinstance( value, Manager ):
		return _aggregate( value, parent, field )
	return value

class ModelSearchResult (seeker.SearchResult):
	
	def __init__( self, queryset ):
		self.rows = list( queryset )
		self.sorted_by = None
		self.sort_formatted = False
	
	def count( self ):
		return len( self.rows )
	
	def _cmp( self, left, right, field, formatted ):
		left_val = _follow( left, field )
		if left_val is None: return -1
		if hasattr( left_val, 'pk' ):
			if not formatted: left_val = left_val.pk
			else: left_val = unicode( left_val )
		right_val = _follow( right, field )
		if right_val is None: return 1
		if hasattr( right_val, 'pk' ):
			if not formatted: right_val = right_val.pk
			else: right_val = unicode( right_val )
		if left_val == right_val: return 0
		return cmp( left_val, right_val )
	
	def sort( self, field, formatted ):
		if (field == self.sorted_by) and (formatted == self.sort_formatted):
			return
		(field, descending) = parse_sort_field( field )
		self.rows.sort( cmp=lambda x,y: self._cmp(x,y,field,formatted), reverse=descending )
	
	def get_value( self, rownum, field, formatted ):
		val = _follow( self.rows[rownum], field )
		if val is None:
			return u''
		if not formatted and hasattr( val, 'pk' ):
			val = val.pk
		return unicode( val )

def qname( prefix, name, sep='__' ):
	if prefix:
		return '%s%s%s' % (prefix, sep, name)
	return name

class ModelSearchPlugin (seeker.SearchPlugin):
	
	def __init__( self, model, select_depth=1, context=None ):
		self.model = model
		self.search_fields = []
		self.display_fields = []
		self.exclude = set()
		self.initial = [f.name for f in self.model._meta.fields]
		self.select_depth = select_depth
		self.context = context
		
		# The stack contains tuples specifying (query_prefix, group_name_prefix, model, displayable)
		stack = [ ('','',model,True) ]
		while len(stack) > 0:
			(prefix, group_prefix, model, displayable) = stack.pop()
			fake_display = []
			if hasattr( model, 'Search' ):
				# If this model has a follow attribute, add the referenced models to the stack
				follow = getattr( model.Search, 'follow', [] )
				for field in follow:
					try:
						f = model._meta.get_field( field )
						rec = ( qname(prefix,field), qname(group_prefix,f.verbose_name,' '), f.rel.to, displayable )
						stack.append( rec )
					except Exception, ex:
						pass
				# Follow any specified reverse relationships as well
				rels = model._meta.get_all_related_objects()
				for r in rels:
					name = r.get_accessor_name()
					if name in follow:
						# Only allow the display of reverse-related fields if the relationship is one-to-one
						rec = ( qname(prefix,name), qname(group_prefix,r.model._meta.verbose_name,' '), r.model, displayable and not r.field.rel.multiple )
						stack.append( rec )
					# Add a "fake" display field on the parent model to show a collapsed
					# version of all the related objects
					if r.field.rel.multiple:
						fake_display.append( qname(prefix,name) )
				# Add this model's excluded fields to the global exclude set
				for f in getattr( model.Search, 'exclude', [] ):
					self.exclude.add( qname(prefix,f) )
			# Initially create the field list for this model to include everything. The exclusions
			# will be handled globally after all the lists are built.
			if not group_prefix:
				group_prefix = self.model._meta.verbose_name
			name = capitalize( '%s fields' % group_prefix )
			self.search_fields.append( (name,[qname(prefix,f.name) for f in model._meta.fields]) )
			if displayable:
				fields = [qname(prefix,f.name) for f in model._meta.fields]
				fields.extend( [qname(prefix,f) for f in fake_display] )
				self.display_fields.append( (name,fields) )
		
		if hasattr( self.model, 'Search' ):
			self.initial = getattr( self.model.Search, 'initial', self.initial )
		
		# Filter out all the excluded fields
		self.initial = [f for f in self.initial if f not in self.exclude]
		self.search_fields = [(name,[f for f in fields if f not in self.exclude]) for (name,fields) in self.search_fields]
		self.display_fields = [(name,[f for f in fields if f not in self.exclude]) for (name,fields) in self.display_fields]
	
	def get_search_fields( self, request ):
		return self.search_fields
	
	def get_display_fields( self, request ):
		return self.display_fields
	
	def get_initial_display_fields( self, request ):
		return self.initial
	
	def _follow_field( self, field ):
		parts = field.split( '__' )
		model = self.model
		field = None
		is_reverse = False
		for p in parts:
			try:
				field = model._meta.get_field( p )
				if field.rel is not None:
					model = field.rel.to
				is_reverse = False
			except Exception, ex:
				for r in model._meta.get_all_related_objects():
					if p == r.get_accessor_name():
						field = r.field
						model = r.model
						is_reverse = True
		return (field, is_reverse)
	
	def get_field_name( self, request, field ):
		(f, is_reverse) = self._follow_field( field )
		if is_reverse:
			return capitalize( f.rel.related_name.replace('_',' ') )
		return capitalize( f.verbose_name )
	
	def get_field_type( self, request, field ):
		(f, is_reverse) = self._follow_field( field )
		if is_reverse:
			return seeker.TYPE_AGGREGATE
		elif f.rel is not None:
			if f.rel.raw_id_admin:
				# If raw_id_admin is specified, treat the field as the related field's type.
				t = f.rel.get_related_field().get_internal_type()
				return FIELD_INFO[t][0]
			return seeker.TYPE_LOOKUP
		elif f.choices:
			return seeker.TYPE_LOOKUP
		return FIELD_INFO[f.get_internal_type()][0]
	
	def get_field_operators( self, request, field ):
		(f, is_reverse) = self._follow_field( field )
		if f.rel is not None:
			if f.rel.raw_id_admin:
				# If raw_id_admin is specified, treat the field as the related field's type.
				t = f.rel.get_related_field().get_internal_type()
				return FIELD_INFO[t][1]
			return LOOKUP_OPERATORS
		elif f.choices:
			return LOOKUP_OPERATORS
		return FIELD_INFO[f.get_internal_type()][1]
	
	def get_field_values( self, request, field ):
		(f, is_reverse) = self._follow_field( field )
		if (f.rel is None or f.rel.raw_id_admin) and not f.choices:
			return None
		return f.get_choices( include_blank=(f.null or f.blank), blank_choice=[('','NULL')] )
	
	def is_field_importable( self, request, field ):
		# TODO: this kinda sucks
		return field.endswith( '_id' )
	
	def get_search_context( self, request, search=None ):
		if self.context is None:
			return None
		elif callable( self.context ):
			return self.context( request, search )
		return unicode( self.context )
	
	def _check_lookup( self, field ):
		(f, is_reverse) = self._follow_field( field )
		if (f.rel is not None) and f.rel.raw_id_admin:
			# If this is a foreign key with raw_id_admin set, replace the search field with
			# the related field name, so that the operators will work.
			other = f.rel.get_related_field()
			return '%s__%s' % (field, other.name)
		return field
	
	def _build_query( self, search ):
		q_list = []
		excludes = {}
		for crit in search.criteria:
			# Create a new Q object for each criteria field (these will be ANDed together)
			q = Q()
			if crit.operator == 'isnot':
				# Special handling for the "isnot" operator -- add a __in to the exclude clause of the QuerySet
				key = str( '%s__in' % crit.field )
				excludes[key] = crit.values
				continue
			field = self._check_lookup( crit.field )
			key = '%s__%s' % (field, crit.operator)
			for value in crit.values:
				# For each value given for one criteria field, join the Q objects with an OR
				q |= Q( **{ str(key): value } )
			q_list.append( q )
		return self.model.objects.filter( *q_list ).exclude( **excludes ).distinct()
	
	def do_search( self, request, search ):
		qs = self._build_query(search).select_related( depth=self.select_depth )
		return ModelSearchResult( qs )
	
	def do_count( self, request, search ):
		qs = self._build_query( search )
		return qs.count()
	
	def save_search( self, request, base_url, search, name ):
		ctx = self.get_search_context( request, search )
		obj = SavedSearch( name=name, context=ctx, url=base_url )
		obj.search_json = search.flatten()
		obj.save()
		return obj.id
	
	def load_search( self, request, base_url, search_id ):
		obj = SavedSearch.objects.get( pk=search_id, url=base_url )
		return seeker.Search( obj.search_json )
	
	def get_searches( self, request, base_url ):
		ctx = self.get_search_context( request )
		qs = SavedSearch.objects.filter( url=base_url, context=ctx )
		return list( qs )
